from math import ceil
from random import random, uniform, gauss, randint
from copy import deepcopy, copy

class Population():
    """The Population class contains a set of candidate patterns which constitute the evolutionary population."""
    def __init__(self, parameters, data):
        """Initialize the Population class."""
        self.p = parameters
        self._parameters_set()
        self.pattern_set = []
        for i in range(self.n_patterns):
            self.pattern_set.append(Pattern(self.p))
        self.data = data
        self.evaluate()
        self.sort()
        self.best_pattern = deepcopy(self.pattern_set[0])
        # initialize population to random starting point
    
    def _parameters_set(self):
        self.dims = self.p.c_hor
        self.n_patterns = self.p.size
        self.frac_discard = self.p.frac_discard
        self.prob_set = self.p.mut_prob
    
    def step(self):
        """Run through one generation."""
        #self.discard()
        self.cross()
        self.mutate()
        self.evaluate()
        self.sort()
        self.best()
    
    def evaluate(self):
        """Evaluate the fitness of the population."""
        for i in range(len(self.pattern_set)):
            if self.pattern_set[i].changed == True:
                self.pattern_set[i].fitness = self.data.fitness(self.pattern_set[i])
                self.pattern_set[i].changed = False
    
    def sort(self):
        """Sort the population by fitness in descending order"""
        for i in range(len(self.pattern_set)):
            best = i
            for j in range(i, len(self.pattern_set)):
                if self.pattern_set[best].fitness < self.pattern_set[j].fitness:
                    best = j
            if best != i:
                self.pattern_set[best], self.pattern_set[i] = self.pattern_set[i], self.pattern_set[best]
    
    def discard(self):
        """Remove the worst fraction of the population."""
        locus = int(ceil((1 - self.frac_discard) * self.n_patterns))
        del(self.pattern_set[locus:])
    
    def cross(self):
        """Perform uniform crossover on the population."""
        # get fitnesses for entire population, find min and max values
        fitness_org = [pattern.fitness for pattern in self.pattern_set]
        min_fitness = min(fitness_org)
        max_fitness = max(fitness_org)
        
        # base fitness after windowing
        diff_critical = (max_fitness - min_fitness) * 0.1
        
        # apply windowing to the fitnesses
        fitness = []
        for fitness_val in fitness_org:
            fitness.append(fitness_val - min_fitness + diff_critical)
        
        # turn the vector into a running sum
        for i in range(1, len(fitness)):
            fitness[i] += fitness[i-1]
        
        while len(self.pattern_set) < self.n_patterns:
            markers = [uniform(0, fitness[-1]), uniform(0, fitness[-1])]
            loci = []
            for marker in markers:
                i = 0
                while fitness[i] < marker:
                    i += 1
                loci.append(i)
            self.pattern_set.append(self.uni_cross(self.pattern_set[loci[0]], self.pattern_set[loci[1]]))
    
    def mutate(self):
        """Mutate the population."""
        for pattern in self.pattern_set:
            for cond in pattern.cond_set:
                if random() < self.prob_set[0]:
                    cond.mutate_active()
                    pattern.changed = True
                if random() < self.prob_set[1]:
                    cond.mutate_range()
                    pattern.changed = True
                if random() < self.prob_set[2]:
                    cond.mutate_mean()
                    pattern.changed = True
    
    def uni_cross(self, pattern1, pattern2):
        #pattern_out = TrawlPattern(self.options)
        pattern_out = deepcopy(pattern1)
        for i in range(self.dims):
            if random() < 0.5:
                pattern_out.cond_set[i] = deepcopy(pattern2.cond_set[i])
            #else:
            #    pattern_out.cond_set[i] = deepcopy(pattern1.cond_set[i])
        pattern_out.changed = True
        return pattern_out
    
    def print_all(self):
        for pattern in self.pattern_set:
            print pattern.fitness
    
    def best(self):
        """Return the best individual in the population."""
        if self.best_pattern.fitness < self.pattern_set[0].fitness:
            self.best_pattern = deepcopy(self.pattern_set[0])
    

class Pattern():
    """The Pattern class represents an individual candidate pattern for matching against the data set."""
    def __init__(self, parameters):
        """Initialize the Pattern class."""
        self.p = parameters
        self.fitness = None
        self.changed = True
        self.cond_set = []
        for i in range(self.p.c_hor):
            self.cond_set.append(Condition(self.p, i))
    
    def __repr__(self):
        """Define a custom representation for the Condition class."""
        return self.expression()
    
    def _parameters_set(self):
        """Assign parameters for operation."""
        self.dims = self.p.c_hor
    
    def expression(self):
        """The expression which is generated when an instance of this class is printed."""
        output = 'C = {'
        str_cond_set = []
        for i in range(len(self.cond_set)):
            if self.cond_set[i].active == True:
                str_cond_set.append('( ' + self.cond_set[i].expression() + ' )')
        for i in range(len(str_cond_set)):
            output += ' ' + str_cond_set[i] + ' '
            if i != len(str_cond_set) - 1:
                output += '^'
        output += '}'
        return output

    def check(self, point):
        """Check whether or not a given point falls in the valid range for this pattern."""
        flag = True
        for i in range(len(point)):
            if self.cond_set[i].check(point[i]) == False and self.cond_set[i].active == True:
                flag = False
        if flag == True:
            return True
        else:
            return False
    

class Condition():
    """The Condition class is the genetic unit of a pattern, filling the same role as a single bit in a canonical genetic algorithm bitstring."""
    def __init__(self, parameters, index):
        """Initialize the Condition class."""
        self.p = parameters
        self.index = index
        self._parameters_set()
        self.val_range = uniform(self.init_range[0], self.init_range[1])
        self.val_mean = uniform(self.init_mean[0], self.init_mean[1])
        self.active = bool(randint(0, 1))
    
    def __repr__(self):
        """Define a custom representation for the Condition class."""
        return self.expression()
    
    def _parameters_set(self):
        """Assign parameters for operation."""
        self.init_range = self.p.init_range
        self.init_mean = self.p.init_mean
        self.mut_range_rate = self.p.mut_range_rate
        self.mut_mean_rate = self.p.mut_mean_rate
    
    def expression(self):
        """The expression which is generated when an instance of this class is printed."""
        out_string = str(round(self.val_mean-self.val_range, 4)) + ' < x_' + str(self.index) + ' < ' + str(round(self.val_mean+self.val_range, 2))
        return out_string
    
    def check(self, value):
        """Check whether or not a given value falls in the valid range for this condition."""
        val_min = self.val_mean - self.val_range
        val_max = self.val_mean + self.val_range
        if val_min < value < val_max:
            return True
        else:
            return False
    
    def mutate_range(self):
        """Mutate the range variable."""
        change = self.val_range * self.mut_range_rate * uniform(0.5, 1.5)
        if random() < 0.5:
            # widen range
            self.val_range += change
        else:
            # shorten range
            self.val_range -= change
    
    def mutate_mean(self):
        """Mutate the mean variable."""
        mu = self.val_mean * self.mut_mean_rate
        sigma = self.val_mean * self.mut_mean_rate / 3
        change = gauss(mu, sigma)
        if random() < 0.5:
            # increase mean
            self.val_mean += change
        else:
            # decrease mean
            self.val_mean -= change
    
    def mutate_active(self):
        """Flip the active bit."""
        self.active = not self.active
    
